/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.mapping;

import java.util.*;
import javax.swing.*;
import javax.swing.text.*;
import org.jdesktop.beansbinding.AutoBinding.UpdateStrategy;
import org.jdesktop.beansbinding.*;
import org.jdesktop.beansbinding.Binding.SyncFailure;
import org.jdesktop.swingbinding.*;
import ch.simpleel.*;
import ch.simpleel.util.*;
import ch.trackedbean.binding.components.*;
import ch.trackedbean.binding.internal.mapping.*;
import ch.trackedbean.loaders.*;
import ch.trackedbean.loaders.lists.*;
import ch.trackedbean.tracking.*;
import ch.trackedbean.tracking.internal.*;

/**
 * This group manages the mappings of a single Bean.<br>
 * Consider using the {@link DataPanel}.
 * 
 * @param <T> The type of the bean used as model.
 * @author M. Hautle
 */
@SuppressWarnings("unchecked")
public class MappingGroup<T> {
    /** The bindings */
    private final BindingGroup grp = new BindingGroup();

    /** List holding unmanaged bindings. */
    private final List<Binding> unmanagedBindings = new ArrayList<Binding>();

    /** Map containing informations for the bindings in this group */
    private final Map<Binding, BindingInfos> bindingInfos = new IdentityHashMap<Binding, BindingInfos>();

    /** The binding listener used for validation purposes. */
    private final ValidationListener validationListener = new ValidationListener();

    /** The list of validators */
    private final List<IBeanValidator> validators = new ArrayList<IBeanValidator>(1);

    /** The validator for single properties or null. */
    private IBeanPropertyValidator propertyValidator;

    /** Flag indicating if the mapping group is read only */
    private final boolean readOnly;

    /** The mapped bean */
    private T model;

    /** The status of {@link #model}. */
    private IBeanStatus status;

    // TODO make readonly setable -> must recreate the mappings for each change :/

    /**
     * Default constructor
     * 
     * @param readOnly True if the group bindings are just read only
     */
    public MappingGroup(final boolean readOnly) {
        this.readOnly = readOnly;
        grp.addBindingListener(validationListener);
    }

    /**
     * Sets the given model.<br>
     * If null is passed then only the current model will be removed.<br>
     * If the group is already bound to a model then the group will be rebound to the new model.<br>
     * <br>
     * Some notes about the 'reachability' of the model:
     * <ul>
     * <li>The old model stays set until all bindings were unbound.</li>
     * <li>The new model gets set before any bindings were unbound.</li>
     * </ul>
     * 
     * @param model The model to set or null to unset the current model
     */
    public void setModel(final T model) {
        // first unbind the current model
        if (this.model != null)
            removeModel();
        this.model = model;
        this.status = TrackedBeanUtils.getBeanStatus(model);
        // only rebind if a model was passed
        if (model != null)
            bind();
    }

    /**
     * Removes the current model
     */
    private void removeModel() {
        // unbind the managed bindings
        unbind(grp.getBindings());
        // unbind the unmanaged bindings
        unbind(unmanagedBindings);
        // unbind the sub bindings
        for (BindingInfos b : bindingInfos.values())
            b.unbindSubBinding();
    }

    /**
     * Returns the model of this mapping group
     * 
     * @return The model of this group
     */
    public T getModel() {
        return model;
    }

    /**
     * Returns the bean status of the mapped bean
     * 
     * @return The bean status or null
     */
    public IBeanStatus getModelStatus() {
        return status;
    }

    /**
     * Returns wherever the mapping is read only or not.
     * 
     * @return True if the mapping is read only
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    /**
     * Binds the current model
     */
    private void bind() {
        // bind the managed bindings
        for (final Binding b : grp.getBindings()) {
            final BindingInfos infos = bindingInfos.get(b);
            infos.configure(b);
            infos.bindSubBinding();
            b.setSourceObject(model);
            b.bind();
        }
        // bind the unmanaged bindings
        for (final Binding b : unmanagedBindings) {
            b.setSourceObject(model);
            b.bind();
        }
    }

    /**
     * Unbinds the given bindings.<br>
     * The value of the targetproperty will be set to null if it implements {@link ICleanUp} or if <code>cleanUp</code> is <code>true</code>.
     * 
     * @param bindings The bindings
     */
    private void unbind(Collection<? extends Binding> bindings) {
        for (final Binding b : bindings) {
            if (b.isBound())
                b.unbind();
            final Object target = b.getTargetObject();
            if (target instanceof ICleanUp)
                b.getTargetProperty().setValue(target, null);
            b.setSourceObject(null);
        }
    }

    /**
     * Validates all bound properties.<br>
     * Call {@link #validateBusinessContent()} to execute all validations.
     */
    public void validateAllProperties() {
        if (model == null)
            return;
        for (Binding b : grp.getBindings()) {
            // ignore StatusProperies (otherwise you get a StackOverflow...)
            if (b.getSourceProperty() instanceof StatusProperty)
                continue;
            validateProperty0(bindingInfos.get(b).getPath());
        }
    }

    /**
     * Validates the business content of the mapped model.
     */
    public void validateBusinessContent() {
        if (model == null)
            return;
        for (final IBeanValidator v : validators)
            v.validate(model, status, null);
    }

    /**
     * Validates the specified property of the model.
     * 
     * @param el The simple el expression for the property to validate
     */
    public void validateProperty(String el) {
        if (model != null)
            validateProperty0(el);
    }

    /**
     * Validates the given property of the model.
     * 
     * @param el The simple el expression
     */
    private void validateProperty0(String el) {
        Object bean = model;
        IBeanStatus status = MappingGroup.this.status;
        // handle nested/indexed properties
        if (!ELHelper.isSimpleProperty(el)) {
            final String path = ELHelper.getParentPath(el);
            bean = ValueAccessorManager.getValueOf(path, model, null);
            status = TrackedBeanUtils.getBeanStatus(bean);
            el = ELHelper.getLastSegment(el);
        }
        propertyValidator.validate(bean, status, el);
    }

    /**
     * Creates a binding between two beans and adds it to the group.<br>
     * 
     * @param srcPath The source path
     * @param dst The destination bean
     * @param dstPath The destination path
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String srcPath, final Object dst, final String dstPath) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, srcPath, dst, dstPath, readOnly);
        final BindingInfos i = new BindingInfos(srcPath);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the status of the source property and the desination property<br>
     * 
     * @param srcPath The source path (where the {@link Status} is taken from)
     * @param dst The destination bean
     * @param dstPath The destination path
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createStatusBinding(final String srcPath, final Object dst, final String dstPath) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createStatusBinding(model, srcPath, dst, dstPath);
        final BindingInfos i = new BindingInfos(srcPath);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the status of the property and the element.<br>
     * 
     * @param <E> The type of the element
     * @param srcPath The source path (where the status is taken from, may also be {@link IBeanStatus#BEAN_STATUS} or {@link IBeanStatus#BEAN_STRUCTURE_STATUS})
     * @param element The target element
     * @return The passed element
     * @throws IllegalStateException if the group is already bound
     */
    public <E extends IStatusDependent> E createStatusDependentBinding(final String srcPath, final E element) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        unmanagedBindings.add(MappingUtils.createStatusDependentBinding(model, srcPath, element));
        return element;
    }

    /**
     * Binds the given {@link IDependentElement} to the given property.
     * 
     * @param <E> The type of the element
     * @param path The path to the property to bind
     * @param element The element to bind
     * @return The passed element
     */
    public <E extends IDependentElement> E createDependentElementBinding(String path, E element) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        unmanagedBindings.add(MappingUtils.createDependentElementBinding(model, path, element));
        return element;
    }

    /**
     * Creates a binding between the given property and the given field and adds it to the group.<br>
     * 
     * @param path The path to the property
     * @param field The {@link JTextComponent} to link
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String path, final JTextComponent field) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, path, field, readOnly);
        final BindingInfos i = new BindingInfos(path);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the given property and the given label and adds it to the group.<br>
     * 
     * @param path The path to the property
     * @param label The JLabel to link
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String path, final JLabel label) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, path, label);
        final BindingInfos i = new BindingInfos(path);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the given property and the given combobox and adds it to the group.<br>
     * Consider using {@link #createBinding(String, JComboBox, Class, Object, Map)}.
     * 
     * @param path The path to the property
     * @param combobox The {@link JComboBox} to link
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String path, final JComboBox combobox) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, path, combobox, readOnly);
        final BindingInfos i = new BindingInfos(path);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the given property and the given list and adds it to the group.<br>
     * 
     * @param path The path to the property
     * @param list The {@link JList} to link
     * @param detailBinding The EL expression (relative to a list element) to use for the cell renderer or null
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String path, final JList list, final String detailBinding) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, path, list, detailBinding);
        final BindingInfos i = new BindingInfos(path);
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the given property and the given combobox and adds it to the group.<br>
     * The combobox content list will be loaded using {@link DataManager#getList(Class, String, Map)}.
     * 
     * @param path The path to the property
     * @param combobox The {@link JComboBox} to link
     * @param type The content type of the combobox list
     * @param subType Additional discriminator specifying the "subtype" of the combobox content list (may be null)
     * @param settings Additional settings for the combobox content load (may be null)
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @see DataManager#getList(Class, String, Map)
     * @see IListLoader
     * @throws IllegalStateException if the group is already bound
     */
    public BindingInfos createBinding(final String path, final JComboBox combobox, Class<?> type, Object subType, Map settings) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        final Binding b = MappingUtils.createBinding(model, path, combobox, readOnly);
        final JComboBoxBinding<Object, List<Object>, JComboBox> listBinding = SwingBindings.createJComboBoxBinding(UpdateStrategy.READ, null, combobox);
        final BindingInfos i = new BindingInfos(path);
        i.setSubBinding(new ListBindingInfos(listBinding, type, subType, settings));
        bindingInfos.put(b, i);
        grp.addBinding(b);
        return i;
    }

    /**
     * Creates a binding between the given property and the table mapping.<br>
     * Use this binding if your bean contains a property holding a list which should be displayed using a table.
     * 
     * @param path The path to the property
     * @param table The table mapping to bind
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     */
    public BindingInfos createTableBinding(final String path, TableMapping table) {
        return createBinding(path, table, TableMapping.MODEL_PROPERTY);
    }

    /**
     * Adds the given binding<br>
     * Please use this method only if it's necessary!<br>
     * -The binding to add has to be unbound!<br>
     * -The source object of the binding will be overridden by this group<br>
     * 
     * @param path The path of the property bound in the given binding
     * @param binding The binding to add
     * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object)} is called)
     * @throws IllegalStateException if the binding or the group is already bound
     * @throws IllegalArgumentException if the binding has no name
     */
    public BindingInfos createBinding(final String path, final Binding binding) {
        if (model != null)
            throw new IllegalStateException("Can't add a binding if the group is already bound");
        if (binding.isBound())
            throw new IllegalStateException("Binding is already bound!");
        final BindingInfos i = new BindingInfos(path);
        bindingInfos.put(binding, i);
        grp.addBinding(binding);
        return i;
    }

    /**
     * Adds a business validator
     * 
     * @param validator The validator to add
     */
    public void addBusinessValidator(final IBeanValidator<T> validator) {
        validators.add(validator);
    }

    /**
     * Removes the business validator
     * 
     * @param validator The validator to add
     */
    public void removeBusinessValidator(final IBeanValidator validator) {
        validators.remove(validator);
    }

    /**
     * Returns the current property validator.
     * 
     * @return The validator or null
     */
    public IBeanPropertyValidator getPropertyValidator() {
        return propertyValidator;
    }

    /**
     * Sets the property validator to use.
     * 
     * @param propertyValidator The validator or null if no validation desired
     */
    public void setPropertyValidator(IBeanPropertyValidator propertyValidator) {
        this.propertyValidator = propertyValidator;
    }

    /**
     * Returns the binding information for all managed bindings.
     * 
     * @return The bindings
     */
    public List<BindingInfos> getManagedBindings() {
        return new ArrayList<BindingInfos>(bindingInfos.values());
    }

    /**
     * {@link BindingListener} used to notify {@link IBeanValidator}s if a property has changed.
     * 
     * @author M. Hautle
     */
    private class ValidationListener implements BindingListener {
        /**
         * Sets the error flag is a conversion fails
         * 
         * @see org.jdesktop.beansbinding.BindingListener#syncFailed(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.Binding.SyncFailure)
         */
        public void syncFailed(final Binding binding, final SyncFailure failure) {
            // ignore status properties (doesen't make sense to watch them..)
            if (model == null || binding.getSourceProperty() instanceof StatusProperty)
                return;
            String property = bindingInfos.get(binding).getPath();
            IBeanStatus status = MappingGroup.this.status;
            // if it's a nested property then get the corresponding status object
            final int lastIndex = property.lastIndexOf('.');
            if (lastIndex > 0)
                try {
                    status = TrackedBeanUtils.getBeanStatus(model, property.substring(0, lastIndex));
                    property = property.substring(lastIndex + 1);
                } catch (final Exception e) {
                    e.printStackTrace();
                    return;
                }
            if (status != null)
                status.setStatus(property, BeanStatusFlag.TECHNICAL_ERROR, failure.getConversionException().getMessage());
        }

        /**
         * Informs the validators about changes
         * 
         * @see org.jdesktop.beansbinding.BindingListener#synced(org.jdesktop.beansbinding.Binding)
         */
        public void synced(final Binding binding) {
            // ignore StatusProperies (otherwise you get a StackOverflow...)
            if (model == null || binding.getSourceProperty() instanceof StatusProperty)
                return;
            final String src = bindingInfos.get(binding).getPath();
            if (propertyValidator != null)
                MappingGroup.this.validateProperty0(src);
            for (final IBeanValidator v : validators)
                v.validate(model, status, src);
        }

        /**
         * Not implemented
         * 
         * @see org.jdesktop.beansbinding.BindingListener#bindingBecameBound(org.jdesktop.beansbinding.Binding)
         */
        public void bindingBecameBound(final Binding binding) {
        }

        /**
         * Not implemented
         * 
         * @see org.jdesktop.beansbinding.BindingListener#bindingBecameUnbound(org.jdesktop.beansbinding.Binding)
         */
        public void bindingBecameUnbound(final Binding binding) {
        }

        /**
         * Not implemented
         * 
         * @see org.jdesktop.beansbinding.BindingListener#sourceChanged(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.PropertyStateEvent)
         */
        public void sourceChanged(final Binding binding, final PropertyStateEvent event) {
        }

        /**
         * Not implemented
         * 
         * @see org.jdesktop.beansbinding.BindingListener#targetChanged(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.PropertyStateEvent)
         */
        public void targetChanged(final Binding binding, final PropertyStateEvent event) {
        }
    }
}
